/*
 * A simple "minesweeper" game in Micro-C
 *
 * Copyright 1999-2003 Dave Dunfield
 * All rights reserved.
 *
 * Permission granted for personal (non-commercial) use only.
 *
 * Compile command: cc msweep -fop
 */
#include <stdio.h>
#include <window.h>

/* Architectural parameters */
#define	MIN_X			4		/* Minimum size of X dimention */
#define	MAX_X			80		/* Maximum size of X dimension */
#define	MIN_Y			4		/* Minimum size of Y dimension */
#define	MAX_Y			24		/* Maximum size of Y dimension */

/* Display characters */
#define	DCOVER			0xFE	/* Character to display covered area */
#define	DSPACE			0xFA	/* Character to display a space */
#define	DMINE			0x0F	/* Character to display mine */
#define	DMARK			'!'		/* Character to display a mark */

/* Bits in Field entries */
#define	MINE			0x8000	/* Location contains a mine */
#define	PENDING			0x4000	/* Location is to be displayed */
#define	SWEEP			0x2000	/* Location is active during sweep */
#define	SWEEP1			0x1000	/* Location has already been processed */
#define	DISPLAY			0x0800	/* Location has been displayed */
#define	MARKED			0x0400	/* Location is marked as mine */
#define	COUNT			0x000F	/* Mask for adjcent mine count */

/* Button indicators returned by mouse_status() */
#define	MOUSE_LEFT		0x01	/* Select button */
#define	MOUSE_RIGHT		0x02	/* Cancel button */
#define	MOUSE_CENTER	0x04	/* Not used in MINESWEEP */

/*
 * Global variables
 */
unsigned
	Field[80][24];		/* Minefield (area of play) */
int
	Xlimit = 35,		/* X size of field */
	Ylimit = 20,		/* Y size of field */
	Mines,				/* Number of mines to plant */
	Xpos,				/* X position of cursor */
	Ypos;				/* Y position of cursor */
unsigned char
	Cursor = -1,		/* Alternate cursor */
	Debug = -1,			/* Debug display enable */
	Mouse = -1,			/* Mouse enable */
	Easy;				/* Easy mode enable */
struct WINDOW
	*mwin;				/* Message window */
extern unsigned
	RAND_SEED;			/* Random number generator seed */
int						/* Direction -> x/y adjustment tables */
	x_round[] = { -1,  0,  1, -1,  1, -1,  0,  1 },
	y_round[] = { -1, -1, -1,  0,  0,  1,  1,  1 };

/*
 * Welcome message
 */
char *welcome[] = {
"\x22\x05MINESWEEPER",
"\x18\x0ACopyright 1999-2003 Dave Dunfield",
"\x1E\x0CAll rights reserved.",
"\x18\x12Too list options, type:  MSWEEP ?",
"\x21\x17(Press a key)",
0 };

/*
 * Help text
 */
char help[] = { "\n\
Use: MSWEEP [options]\n\n\
opts:	/C	- use alternate Cursor\n\
	/D	- enable Debug display\n\
	/E	- Easy mode (sweeps even when next to mine)\n\
	/I	- display Instructions\n\
	/M	- disable Mouse\n\
	M=n	- set # Mines\n\
	R=n	- set Random number seed\n\
	X=#	- set X dimension (4-80)\n\
	Y=#	- set Y dimension (4-24)\n\n\
Copyright 1999-2003 Dave Dunfield\n\All rights reserved.\n" };

/*
 * Instruction text
 */
char instructions[] = { "\n\
The object of the game is to uncover all the locations on the field which\n\
do not contain mines in the shortest amount of time. Uncovering a location\n\
which contains a mine will cause the game to end (you lose).\n\n\
When you uncover a location, a number will appear indicating the number of\n\
mines in the adjacent squares. If no adjacent squares contain mines, the\n\
search will 'sweep' to all adjacent squares which do have bordering mines.\n\n\
As a aid, you may also mark any uncovered squares as possible mines. This\n\
will display a special marker in that position as a reminder.\n\n\
The game will end if you uncover a mine (boom), or when only uncovered mines\n\
remain. You can also end the game by pressing ESC.\n\n\
When the game ends, the board is completely uncovered. Any locations which\n\
were incorrect will be displayed in reverse video:\n\
 - Mines where were uncovered\n\
 - Non-mines which were NOT uncovered\n\
 - Non-mines which were marked as possible mines\n\
If there is no reverse video on the final board, note your time - !YOU WON!\n\n\
KEYBD: Uncover=space Mark=enter  Move=up/down/left/right/home/end/pgup/pgdn\n\
MOUSE: Uncover=left  Mark=right  Move=motion" };
/*
 * Plant a mine in the field at specified coordinates
 */
int plant_mine(int x, int y)
{
	int i, x1, y1;
	if(Field[x][y] & MINE)
		return -1;
	Field[x][y] |= MINE;
	/* Incremt mine counts in surrounding directions */
	for(i=0; i < 8; ++i) {
		x1 = x + x_round[i];
		y1 = y + y_round[i];
		if((x1 >= 0) && (y1 >= 0) && (x1 < Xlimit) && (y1 < Ylimit))
			++Field[x1][y1]; }
	return 0;
}

/*
 * Generate the correct "status" character to show the content
 * of a particular field location.
 */
char status(int x, int y)
{
	unsigned c;
	c = Field[x][y];
	if(c & MARKED)
		return DMARK;
	if(Debug && !(c & DISPLAY))
		return DCOVER;
	if(c & MINE)
		return DMINE;
	if(c &= COUNT)
		return c + '0';
	return DSPACE;
}

/*
 * Sweep the minefield, starting at a given location. All locations
 * with are logically available from this one are displayed.
 * Return TRUE if there are no non-mine locations remaining (game over).
 */
int sweep(int x1, int y1)
{
	int i, x, y;
	unsigned c;
	unsigned char Sweep_flag;

	/* Clear flags */
	for(y=0; y < Ylimit; ++y)
		for(x=0; x < Xlimit; ++x)
			Field[x][y] &= ~(SWEEP|SWEEP1|PENDING);

	Field[x1][y1] |= SWEEP;		/* Start with this location */

	/* Scan field, displaying locations at which we arrive */
	/* Continue scanning until we detect no new locations */
	do {
		for(Sweep_flag=y=0; y < Ylimit; ++y) {
			for(x=0; x < Xlimit; ++x) {
				c = Field[x][y];
				if((c & (SWEEP|SWEEP1)) == SWEEP) {	/* Process this one */
					Field[x][y] |= (DISPLAY|SWEEP1);/* Mark as swept */
					wgotoxy(x, y);
					wputc(status(x, y));
					/* Attempt to sweep to adjcent locations */
					if(Easy || !(c & COUNT)) for(i=0; i < 8; ++i) {
						x1 = x + x_round[i];
						y1 = y + y_round[i];
						if((x1 >= 0) && (y1 >= 0) && (x1 < Xlimit) && (y1 < Ylimit)) {
							c = Field[x1][y1];
							if(c & (MINE|SWEEP1|MARKED|DISPLAY))
								continue;
							if(c & COUNT)
								Field[x1][y1] |= PENDING;
							else {
								Field[x1][y1] |= SWEEP;
								Sweep_flag = -1; } } }
					Field[x][y] &= ~SWEEP; } } }	/* Deactivate this one */
		delay(50); }
	while(Sweep_flag);

	/* Finally, display all count locations which make up the border,
	 * and determine if we have finished */
	Sweep_flag = -1;
	for(y=0; y < Ylimit; ++y)
		for(x=0; x < Xlimit; ++x) {
			c = Field[x][y];
			if(c & PENDING) {
				Field[x][y] |= DISPLAY;
				wgotoxy(x, y);
				wputc(status(x, y)); }
			if(!(c & (DISPLAY|MINE)))
				Sweep_flag = 0; }

	return Sweep_flag;
}

/*
 * Initializes the mouse driver, returns with -1 if successful.
 */
int init_mouse() asm
{
; Initialize & test for mouse
		XOR		AX,AX			; Init functions.
		INT		33h				; Call mouse driver
		AND		AX,AX			; Mouse present
		JZ		initm1			; No, skip it
; Set mouse limits (some drivers do not do it properly on reset)
		XOR		CX,CX			; Lower limit is zero
		MOV		DX,639			; Upper horizontal limit
		MOV		AX,7			; Set horizontal limit
		INT		33h				; Call mouse driver
		MOV		DX,479			; Upper vertical limit
		MOV		AX,8			; Set vertical limit
		INT		33h				; Call mouse driver
		MOV		AX,-1			; Indicate mouse ok
initm1:
}

/*
 * Update the cursor position (call from mouse handler)
 */
void move_cursor(unsigned x, unsigned y)
{
	static unsigned lx = 600, ly = 400;
	if(x > lx)
		lx = x;
	if(y > ly)
		ly = y;
	Xpos = (x * Xlimit) / lx;
	Ypos = (y * Ylimit) / ly;
	if(Xpos < 0)
		Xpos = 0;
	if(Xpos >= Xlimit)
		Xpos = Xlimit-1;
	if(Ypos < 0)
		Ypos = 0;
	if(Ypos >= Ylimit)
		Ypos = Ylimit-1;
	wgotoxy(Xpos, Ypos);
}

/*
 * Update mouse position and on-screen cursor. If any button is
 * activated, remove cursor, wait for button to be released, and
 * report it.
 */
unsigned mouse_status()
{
	int x, y, z;
	static unsigned mousex, mousey;

	/* Get mouse position and button status */
	asm {
		MOV		AX,0003h	; Mouse status function
		INT		33h			; Call mouse driver
		MOV		-2[BP],BX	; Save buttons
		MOV		-4[BP],DX	; Save Y position
		MOV		-6[BP],CX	; Save X position
		}

	/* If cursor position changed, update cursor and display*/
	if((x != mousex) || (y != mousey))
		move_cursor(mousex = x, mousey = y);

	/* If any buttons are activated, wait for release, remove cursor */
	if(z & (MOUSE_LEFT|MOUSE_RIGHT|MOUSE_CENTER)) {
		asm {
			mloop1:	MOV		AX,0003h		; Mouse status function
					INT		33h				; Call mouse driver
					AND		BL,07h			; Any buttons down?
					JNZ		mloop1			; Wait till clear
			} }

	/* Pass back button status to caller */
	return z;
}

/*
 * Main program.
 */
main(int argc, char *argv[])
{
	int i;
	unsigned c, x, y;
	char *ptr;
	static unsigned time = 0;
	static char tflag = 0;

	RAND_SEED = peekw(0x40, 0x6C);	/* Randomize the generator */

	for(i=1; i < argc; ++i) {
		ptr = argv[i];
		switch((toupper(*ptr++) << 8) | toupper(*ptr++)) {
			case 'X=' : Xlimit = atoi(ptr);		continue;
			case 'Y=' : Ylimit = atoi(ptr);		continue;
			case 'M=' : Mines = atoi(ptr);		continue;
			case 'R=' : RAND_SEED = atoi(ptr);	continue;
			case '-C' :
			case '/C' : Cursor = 0;				continue;
			case '-D' :
			case '/D' : Debug = 0; 				continue;
			case '-E' :
			case '/E' : Easy = -1;				continue;
			case '-I' :
			case '/I' : abort(instructions);
			case '-M' :
			case '/M' : Mouse = 0;				continue;
			default:
				printf("Unknown option: %s\n", argv[i]);
			case '?'<<8 :
			case '-?' :
			case '/?' :
			do_help:
				abort(help); } }

	/* Test X/Y settings to insure valid */
	if((Xlimit < MIN_X) || (Xlimit > MAX_X))
		goto do_help;
	if((Ylimit < MIN_Y) || (Ylimit > MAX_Y))
		goto do_help;

	wopen(0, 0, 80, 25, NORMAL|WCOPEN|WCCLOSE);
	wcursor_off();
	for(i=0; ptr=welcome[i]; ++i) {
		wgotoxy(*ptr++, *ptr++);
		wputs(ptr); }

	if(!init_mouse())
		Mouse = 0;

	/* Plant the mines */
	if(!Mines)	/* Not specified, calculate # mines */
		Mines = (Xlimit * Ylimit) / 7;
	for(i=0; i < Mines; ++i) {
		c = 1000;
		while(plant_mine(random(Xlimit), random(Ylimit)))
			if(!--c) {
				wclose();
				abort("Too many mines!"); } }

	wgetc();
	wclwin();
	mwin = wopen(0, 24, 80, 1, REVERSE|WCOPEN);
	wprintf("MINESWEEP:  %-46s(x=%u y=%u m=%u)", Mouse ?
		"Uncover=LEFT  Mark=RIGHT  Exit=ESC" :
		"Uncover=SPACE  Mark=ENTER  Exit=ESC", Xlimit, Ylimit, Mines);

	/* Open bordered window if small, full-size if large */
	if((Xlimit <(MAX_X-1)) && (Ylimit < (MAX_Y-1)))
		wopen(39-Xlimit/2, 11-Ylimit/2, Xlimit+2, Ylimit+2, NORMAL|WBOX1);
	else
		wopen(0, 0, Xlimit, Ylimit, NORMAL);

	/* Display the initial screen */
	if(!Debug)
		*W_OPEN = REVERSE;
	for(y=0; y < Ylimit; ++y) {
		wgotoxy(0, y);
		for(x=0; x < Xlimit; ++x)
			wputc(status(x, y)); }
	*W_OPEN = NORMAL;

	if(Cursor)
		wcursor_block();
	else
		wcursor_line();
	wgotoxy(Xpos = 0, Ypos = 0);

	/* main processing loop */
	do {
		if(tflag) {
			if((x = peekw(0x40, 0x6C)) != y) {
				y = x;
				if(!--tflag) {
					tflag = 18;
					mwin->WINcurx = 75;
					w_printf(mwin, "%5u", ++time); } } }
		if(Mouse) {
			i = mouse_status();
			c = Field[Xpos][Ypos];
		if(i & MOUSE_LEFT)
			goto do_sweep;
		if(i & MOUSE_RIGHT)
			goto do_mark; }
		c = Field[Xpos][Ypos];
		switch(i = wtstc()) {
		case _KUA :	if(--Ypos < 0) Ypos = Ylimit-1;	goto move;
		case _KDA : if(++Ypos >= Ylimit) Ypos = 0;	goto move;
		case _KLA :	if(--Xpos < 0) Xpos = Xlimit-1;	goto move;
		case _KRA : if(++Xpos >= Xlimit) Xpos = 0;	goto move;
		case _KPU : Ypos = 0;						goto move;
		case _KPD : Ypos = Ylimit-1;				goto move;
		case _KHO : Xpos = 0;						goto move;
		case _KEN : Xpos = Xlimit-1;
		move:		wgotoxy(Xpos, Ypos);			continue;
		case ' ' :		/* Sweep for mine */
		do_sweep:
			tflag = 18;
			Field[Xpos][Ypos] = c & ~MARKED;
			wputc(status(Xpos, Ypos));
			if(c & DISPLAY)		/* Don't allow sweep of displayed */
				continue;
			if(sweep(Xpos, Ypos) || (c & MINE))
				i = 0x1B;
			wgotoxy(Xpos, Ypos);
			continue;
		case '\n'  :	/* Mark as possible mine */
		do_mark:
			tflag = 18;
			if(c & MARKED)
				Field[Xpos][Ypos] = c & ~MARKED;
			else if(!(c & DISPLAY))	/* Don't allow mark of displayed */
				Field[Xpos][Ypos] = c | MARKED;
			wputc(status(Xpos, Ypos));
			wgotoxy(Xpos, Ypos); } }
	while(i != 0x1B);

	/*
	 * Display final screen, and show problems.
	 *
	 * Note, error count indicates TOTAL errors, multiple errors can occur
	 * in one location. eg: a non-location marked as a mine would generate
	 * 2 errors (mis-marked) and (non-mine not uncovered)
	 */
	wcursor_off();
	for(y=Xpos=0; y < Ylimit; ++y) {
		wgotoxy(0, y);
		for(x=0; x < Xlimit; ++x) {
			c = Field[x][y];
			tflag = 0;
			/* Mines which have been displayed */
			if((c & MINE) && (c & DISPLAY))
				++tflag;
			/* Non-mines which have been marked */
			if((c & MARKED) && !(c & MINE))
				++tflag;
			/* Non-mines which have not been uncovered */
			if(!(c & (DISPLAY|MINE)))
				++tflag;
			if(tflag) {
				Xpos += tflag;
				*W_OPEN = REVERSE;
				tflag = 0; }
			Field[x][y] |= DISPLAY;
			wputc(status(x, y));
			*W_OPEN = NORMAL; } }

	/* Display stats, and finish up */
	w_clwin(mwin);
	w_printf(mwin, "Finished with %u error%s... Press any key to EXIT!",
		Xpos, "s"+(Xpos==1));
	w_gotoxy(75, 0, mwin);
	w_printf(mwin, "%5u", time);
	wgetc();
	wclose();
	wclose();
	W_OPEN->WINpcurx = W_OPEN->WINpcury = 0;
	wclose();
}
